/****************************************************************************
 * Filename : bubblecraft.c
 * Date     : 12-11-2010
 *
 * Copyright (C) 2010 João Fernandes, Renato Rodrigues
 *
 * This file is part of Bubblecraft.
 *
 * Bubblecraft is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Bubblecraft is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ****************************************************************************/
 
  /** @defgroup BUBBLECRAFT BUBBLECRAFT
 * @{
 *
 * Programa principal do jogo
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <time.h>
#include <string.h>
#include <dos.h>
#include <assert.h>
#include <math.h>

#include "video-text.h"
#include "video-graphics.h"
#include "utility.h"
#include "parameters.h"
#include "matrix_algorithms.h"
#include "int.h"
#include "timer.h"
#include "speaker.h"
#include "kbc.h"
#include "rtc.h"
#include "codepage.h"
#include "highscores.h"

/* Colours */
#define ORANGE_BUBBLE 0
#define PURPLE_BUBBLE 1
#define WHITE_BUBBLE  2
#define BLACK_BUBBLE  3
#define GREEN_BUBBLE  4
#define YELLOW_BUBBLE 5
#define RED_BUBBLE    6
#define BLUE_BUBBLE   7

/* Scancodes */
#define LEFT_ARROW  0x4B
#define RIGHT_ARROW 0x4D
#define SPACE_BAR   0x39
#define ESCAPE      0x01
#define P_KEY       0x19
#define TAB         0x0F

/****************************************************************************
 * Text mode
 ****************************************************************************/
void showLicense() {
  fillScreen(' ', LIGHT_GRAY_BACK);
  drawFrame("", LIGHT_GRAY_BACK, 0, 0, WIDTH, HEIGHT);
  drawFrame("GNU General Public License", LIGHT_GRAY_BACK, 6, 8, 68, 8);
  
  char copyright[] = "Bubblecraft Copyright (C) 2010 Joao Fernandes, Renato Rodrigues";
  copyright[33] = 0x84;
  
  printStringAt(copyright, 8, 10, LIGHT_GRAY_BACK);
  printStringAt("This program comes with ABSOLUTELY NO WARRANTY.", 8, 11, LIGHT_GRAY_BACK);
  printStringAt("This is free software, and you are welcome to redistribute it", 8, 12, LIGHT_GRAY_BACK);
  printStringAt("under certain conditions.", 8, 13, LIGHT_GRAY_BACK);
  
  pause(PAUSE_MSG, 26, 20, LIGHT_GRAY_BACK);
}

/**
 * Aloca memória para a matriz de bolhas
 */
Bubble **allocBubblesMatrix(int height, int width) {
  Bubble **bubblesMatrix = (Bubble **) malloc(height * width * sizeof(Bubble *));
  assert(bubblesMatrix != NULL);
  int y, x;
  for (y = 0; y < height; ++y)
    for (x = 0; x < width;  ++x)
      bubblesMatrix[y * width + x] = newBubble(rand() % N_BUBBLES);
      
  return bubblesMatrix;
}

/**
 * Acrescenta heightIncrement linhas á matriz.
 * A linha é vazia se empty for true.
 */
void resizeBubblesMatrix(Bubble ***bubblesMatrix, int *height, int width, 
                         int heightIncrement, bool_t empty) {
  if (heightIncrement <= 0) /* temporary */
    return;
  
  int newHeight = *height + heightIncrement;                       
  *bubblesMatrix = (Bubble**) realloc(*bubblesMatrix, newHeight *  width * sizeof(Bubble*));
  //assert(*bubblesMatrix != NULL);
  int x, y;
  for (y = *height; y < newHeight; ++y) {
    for (x = 0; x < width; ++x) {
      if (empty == true)
        (*bubblesMatrix)[y * width + x] = newBubbleEmpty();
      else
        (*bubblesMatrix)[y * width + x] = newBubble(rand() % N_BUBBLES);
    }
  }
  
  *height += heightIncrement;
}

/**
 * Destroi a matriz de bolhas
 */
void freeBubblesMatrix(Bubble ***bubblesMatrix, int height, int width) {
  int y, x;
  for (y = 0; y < height; ++y) {
    for (x = 0; x < width; ++x) {
      free((*bubblesMatrix)[y * width + x]);
      (*bubblesMatrix)[y * width + x] = NULL;
    }
  }
  free(*bubblesMatrix);
  *bubblesMatrix = NULL;
}

/**
 * Desenha a matriz de bolhas
 */
void drawBubblesMatrix(int x1, int y1, int height, int width,
                       Bubble ***bubblesMatrix, BITMAP *bubbles, byte_t *target) {
  int x, y, offset, limit;
  for (y = 0; y < height; ++y) {
    offset = y % 2 == 0 ? 0 : BUBBLE_SIZE >> 1;
    limit = y % 2 == 0 ? width : width - 1;
    x = y % 2 == 0 ? 0 : 1;
    for (; x < limit;  x += 2) {
      if ((*bubblesMatrix)[y * width + x]->colour != -1)
        draw_t_bitmap(&bubbles[(*bubblesMatrix)[y * width + x]->colour],
                      x1 + BUBBLE_SIZE * (x >> 1) + offset,
                      y1 + (BUBBLE_SIZE - 3) * y, 12, target);
    }
  }
}

/**
 * Força o x a ter a mesma paridade que y
 */
void force_odd_or_even (int *y, int *x, int width, double xpart) {
	if (*y % 2 == 0) {
		if (*x % 2 != 0) {
			if (*x - 1 < 0)
				++(*x);
			else if (*x + 1 >= width)
				--(*x);
			else
				*x += 1*(xpart/fabs(xpart));
		}
	}
	else {
		if (*x % 2 == 0) {
			if (*x - 1 < 1)
				++(*x);
			else if (*x + 1 >= width - 1)
				--(*x);
			else
				*x += 1*(xpart/fabs(xpart));
		}
	}
}

typedef enum  {
  PLAYING,
  PAUSED,
  SHOOTING_BUBBLE,
  SHOWING_HIGHSCORES,
  GAME_OVER
} game_state_t;

void wait_for_input() {
  clearGQueue(keyboard_queue);
  while (isEmptyGQueue(keyboard_queue))
    ;
  clearGQueue(keyboard_queue);
}
    
int main() {
  srand(time(0));
  //initializeRtc();

  /*
   * Bitmaps importation and allocation
   */
  BITMAP raw_bubbles = load_bmp("bubbles.bmp", true);  
  BITMAP bubbles[N_BUBBLES];
  int bubble_size = BUBBLE_SIZE * BUBBLE_SIZE;
  int i,j;
  for (i = 0; i < N_BUBBLES; ++i) {
    setBitmapDimensions(&bubbles[i], BUBBLE_SIZE, BUBBLE_SIZE);
    bubbles[i].data = (byte_t *) malloc(bubble_size);
    //assert(bubbles[i].data != NULL);
    memcpy(bubbles[i].data, &(raw_bubbles.data[bubble_size * i]), bubble_size);
  }
  free(raw_bubbles.data);
  
  /* Highscores */  
  Score highscores[15];
  if (!loadScores("highscores.txt", highscores))
    for (i = 0; i < 15; ++i) {
      highscores[i].letter1 = highscores[i].letter2 = highscores[i].letter3 = 'A';
      highscores[i].score = 0;
    }
 
  showLicense();
  clearScreen();
  
  byte_t *table = (byte_t *) malloc(256 * CHAR_HEIGHT);
  assert(table != NULL);
  loadCharTable(table);
  __dpmi_meminfo map;
  VGA = enter_graphics(GRAPHICS_MODE, &map);
  set_palette(raw_bubbles.palette);  
  dword_t screenResolution = HRES * VRES;
  
  byte_t *doubleBuffer = (byte_t *) malloc(screenResolution);
  set_palette(raw_bubbles.palette);  
  assert(doubleBuffer != NULL);
  /* Keyboard initialization */
  set_kbd_isr_q(&prev_kbd_isr);
  /* Mouse initialization */
  mouse_init(mouse_isr_q, &prev_mouse_isr);
  
  int matrixHeight = 1, 
      matrixWidth  = MATRIX_X_SIZE;
  Bubble **bubblesMatrix = allocBubblesMatrix(matrixHeight, matrixWidth);
  
  bool_t boolMatrix[15][MATRIX_X_SIZE];
  for (i = 0; i < MATRIX_X_SIZE; ++i)
    boolMatrix[matrixHeight - 1][i] = true;
  for (i = 1; i < MATRIX_HEIGHT; ++i)
    for (j = 0; j < MATRIX_X_SIZE; ++j)
      boolMatrix[i][j] = false;
  
  int colourNextBubble = rand() % N_BUBBLES;
  int colourCurrentBubble = rand() % N_BUBBLES;
  int x1 = 272, y1 = 40;
  clear_screen(1, doubleBuffer);
  wait_for_retrace();
  memcpy(VGA, doubleBuffer, screenResolution);
  /**/
  byte_t *input;
  double angle = 90;
  int arrowHeight = BUBBLE_SIZE << 1;
  int screenBottom = VRES - 1;
  int centerX = HRES >> 1;
  int arrowY, arrowX, matrixY, matrixX;
  int halfBubble = BUBBLE_SIZE >> 1;
  // Variáveis de onde está a bolha a disparar
  int bubbleX = centerX;
  int bubbleY = screenBottom;
  // Variáveis para cálculos do trajecto da bolha
  double exactx, exacty, xpart, ypart;
  xpart = 0;
  ypart = 0;

  dword_t score = 0;
  char scoreStr[10];
 
  int nextBubbleBoxX1     = 204,
      nextBubbleBoxY1     = screenBottom - 84,
      nextBubbleBoxWidth  = BUBBLE_SIZE + 20,
      nextBubbleBoxHeight = BUBBLE_SIZE + 20;
      
  int nextBubbleBoxTitleX1 = 168,
      nextBubbleBoxTitleY1 = nextBubbleBoxY1 + 52;
      
  int scoreBoxX1     = 182,
      scoreBoxY1     = nextBubbleBoxY1 - 80,
      scoreBoxWidth  = 90,
      scoreBoxHeight = 32;
      
  int scoreBoxTitleX1          = 139,
      scoreBoxTitleY1          = scoreBoxY1 + 16,
      scoreBoxForegroundColour = 0,
      scoreBoxBackgroundColour = 1;
      
  int scoreX1               = 188,
      scoreY1               = scoreBoxY1 + 2,
      scoreForegroundColour = 0,
      scoreBackgroundColour = -1;
      
  
  game_state_t gameState = PLAYING;
  mouse_data_t mouseData;
  setDeltaAlarm(5);
  while (gameState != GAME_OVER) {
    /* Polar Coordinates to Cartesian Coordinates */
    arrowX = arrowHeight * cos((angle * PI) / 180);
    arrowY = arrowHeight * sin((angle * PI) / 180);
    /* Refresh */
    clear_screen(1, doubleBuffer);
    draw_line(271, 0, 271, screenBottom, 0, doubleBuffer);
    draw_line(752, 0, 752, screenBottom, 0, doubleBuffer);
    drawBubblesMatrix(x1, y1, matrixHeight, matrixWidth, &bubblesMatrix, bubbles,
                      doubleBuffer);
    draw_line(centerX, screenBottom - halfBubble, centerX + arrowX,
              screenBottom - halfBubble - arrowY, 0, doubleBuffer);
    /* Draws bubble to shoot */
    draw_ct_bitmap(&bubbles[colourCurrentBubble], bubbleX, bubbleY - halfBubble, 
                   12, doubleBuffer);
    /* Draws next bubble box */
    draw_rectangle(nextBubbleBoxX1, nextBubbleBoxY1, nextBubbleBoxHeight, 
                   nextBubbleBoxWidth, 0, doubleBuffer);
    drawString("next", nextBubbleBoxTitleX1, nextBubbleBoxTitleY1, 0, -1, 1, 
                 doubleBuffer, table);
    draw_t_bitmap(&bubbles[colourNextBubble], nextBubbleBoxX1 + 10, 
                  nextBubbleBoxY1 + 10, 12, doubleBuffer);
                  draw_line(nextBubbleBoxTitleX1, nextBubbleBoxTitleY1 + 15, 
                            nextBubbleBoxX1, nextBubbleBoxTitleY1 + 15, 0, doubleBuffer);
    /* Draws score box */
    draw_rectangle(scoreBoxX1, scoreBoxY1, scoreBoxHeight, scoreBoxWidth, 0, 
                   doubleBuffer);
    drawString("score", scoreBoxTitleX1, scoreBoxTitleY1, scoreBoxForegroundColour,
                 scoreBoxBackgroundColour, 1, doubleBuffer, table);
    draw_line(scoreBoxTitleX1, scoreBoxTitleY1 + 15, scoreBoxX1, scoreBoxTitleY1 + 15, 0,
              doubleBuffer);
    sprintf(scoreStr, "%05lu", score);
    drawString(scoreStr, scoreX1, scoreY1, scoreForegroundColour,
                 scoreBackgroundColour, 2, doubleBuffer, table); 
    /* Game paused */
    if (gameState == PAUSED)
      drawString("PAUSED", 450, 320, 0, -1, 3, doubleBuffer, table);
    else if (gameState == SHOWING_HIGHSCORES) {
      printScores(highscores, doubleBuffer, table);
    }
    /* Double buffer to VGA */
    wait_for_retrace();
    memcpy(VGA, doubleBuffer, screenResolution);
    
    /* Read input */
    if (!isEmptyGQueue(mouse_queue)) {
      getMouseData(&mouseData); 
      if (mouseData.leftButton == 1) {
          if (gameState != PAUSED && gameState != SHOOTING_BUBBLE) {
            gameState = SHOOTING_BUBBLE;
            // Componentes horizontais e verticais da trajectória da bolha
            xpart = cos ((angle * PI) / 180);
            ypart = sin ((angle * PI) / 180);
            exactx = bubbleX;
            exacty = bubbleY;
          }
      }
      if (mouseData.mouseByte2 != 0) {
        angle -= mouseData.mouseByte2;
      }
		} 
    else if (!isEmptyGQueue(keyboard_queue)) {
      input = (byte_t *) getGQueueElement(keyboard_queue);
      switch (*input) {
        case ESCAPE :
          gameState = GAME_OVER;
          break;
        case LEFT_ARROW :
          if (gameState != PAUSED && gameState != SHOWING_HIGHSCORES 
              && angle < 175)
            angle += 5;
          break;
        case RIGHT_ARROW :
          if (gameState != PAUSED && gameState != SHOWING_HIGHSCORES 
              && angle > 5)
            angle -= 5;
          break;
        case SPACE_BAR :
          if (gameState != PAUSED && gameState != SHOOTING_BUBBLE) {
            gameState = SHOOTING_BUBBLE;
            // Componentes horizontais e verticais da trajectória da bolha
            xpart = cos ((angle * PI) / 180);
            ypart = sin ((angle * PI) / 180);
            exactx = bubbleX;
            exacty = bubbleY;
          }
          break;
        case P_KEY:
          if (gameState == PLAYING)
            gameState = PAUSED;
          else if (gameState == PAUSED)
            gameState = PLAYING;
          break;
        case TAB:
          if (gameState == PLAYING)
            gameState = SHOWING_HIGHSCORES;
          else if (gameState == SHOWING_HIGHSCORES)
            gameState = PLAYING;
          break;
      }
    }
    // Se a bolha estiver em movimento
    if (gameState == SHOOTING_BUBBLE) {
      // Altera a posição exacta da bolha.
      exactx += 2*xpart;
      exacty -= 2*ypart;
      // Trunca os valores da posição da bolha
      bubbleX = exactx;
      bubbleY = exacty;
      matrixX = 2 * ((bubbleX - x1) / BUBBLE_SIZE);
      matrixY = (bubbleY - y1) / BUBBLE_SIZE;
      force_odd_or_even(&matrixY, &matrixX, matrixWidth, xpart);
      // Se bater numa linha lateral inverte a componente horizontal
      if (matrixX <= 0 || matrixX >= matrixWidth-1)
        xpart = -xpart;
      if (matrixY < 0)
        matrixY = 0;
      if (matrixX >= 0 && matrixX < matrixWidth) {
        if (matrixY == 0 
            || boolMatrix[matrixY - 1][matrixX + 1] == true
            || boolMatrix[matrixY-1][matrixX-1] == true
			|| boolMatrix[matrixY][matrixX-2] == true
			|| boolMatrix[matrixY][matrixX+2] == true) {
          // Posição da próxima bolha a disparar passa a ser o fundo
          bubbleX = centerX;
          bubbleY = screenBottom;
      
          // Deixa de se mexer
          xpart = 0;
          ypart = 0;
          Bubble *to_add = newBubble(colourCurrentBubble);
      
          if (matrixY >= matrixHeight)
            resizeBubblesMatrix(&bubblesMatrix, &matrixHeight, matrixWidth, 1, true);
          
          checkForPop (&bubblesMatrix, matrixHeight, matrixWidth, matrixX, matrixY , to_add);
      
          for (i = 0; i < matrixHeight; ++i)
            for (j = 0; j < MATRIX_X_SIZE; ++j)
              if (bubblesMatrix[i*matrixWidth + j]->colour != -1)
                boolMatrix[i][j] = true;
              else
                boolMatrix[i][j] = false;
          
          if (matrixHeight == 15)
            gameState = GAME_OVER;
          else {          
            colourCurrentBubble = colourNextBubble;
            colourNextBubble    = rand() % N_BUBBLES;
            gameState = PLAYING;
          }
        }
      }
    } 
    else if (gameState != GAME_OVER && gameState != PAUSED
             && gameState != SHOWING_HIGHSCORES && rtcAlarmIsOn) {
      resizeBubblesMatrix(&bubblesMatrix, &matrixHeight, matrixWidth, 1, false);
      score += 10;
      for (i = 0; i < MATRIX_X_SIZE; ++i)
        boolMatrix[matrixHeight - 1][i] = true;
      rtcAlarmIsOn = false;
      setDeltaAlarm(5);
    }
  } /* Main loop */
  /* Game over */
  clear_screen(0, doubleBuffer);
  drawString("GAME OVER", 410, 320, 1, 0, 3, doubleBuffer, table);
  wait_for_retrace();
  memcpy(VGA, doubleBuffer, screenResolution);
  
  setup_timer0();
  msleep(5000);
  restore_timer0();
  
  leave_graphics(map);
  fillScreen(' ', LIGHT_GRAY_BACK);
  restore_kbd_isr(&prev_kbd_isr);
  finalizeRtc();
  drawFrame("", LIGHT_GRAY_BACK, 0, 0, WIDTH, HEIGHT);
  if (score > highscores[N_SCORES - 1].score) {
    highscores[N_SCORES - 1].score = score; 
    printStringAt("Insira o seu nome: ", 1, 1, LIGHT_GRAY_BACK);
    char name[4];
    highscores[N_SCORES - 1].letter1 = name[0];
    highscores[N_SCORES - 1].letter2 = name[1];
    highscores[N_SCORES - 1].letter3 = name[2];
    sortScores(highscores);
  }

  saveScores(HIGHSCORES_FILE, highscores);
  freeBubblesMatrix(&bubblesMatrix, matrixHeight, matrixWidth);
  for (i = 0; i < N_BUBBLES; ++i) {
    free(bubbles[i].data);
    bubbles[i].data = NULL;
  }
  clearScreen();
  return 0;
}

/** @} End of MATRIX ALGORITHMS */
